Оглавление

  • 1  Обзор и описание данных
  • 2  Предобработка и дополнение данных
    • 2.1  Обработка дубликатов
    • 2.2  Обработка пропущенных значений
    • 2.3  Дополнение данных
  • 3  Анализ данных
    • 3.1  Распределение по категориям
    • 3.2  Количество посадочных мест
    • 3.3  Сетевые и не сетевые заведения
      • 3.3.1  Распределение сетевых заведений по категориям
    • 3.4  Крупнейшие сети
    • 3.5  Районы, кварталы
    • 3.6  Пользовательский рейтинг
      • 3.6.1  Cетевые и не сетевые заведения
      • 3.6.2  Зависимость рейтинга от района
    • 3.7  Места на карте
    • 3.8  Популярные улицы
    • 3.9  Непопулярные улицы
    • 3.10  Средний чек
      • 3.10.1  Зависимость среднего чека от локации
      • 3.10.2  Зависимость среднего чека от типа заведения
    • 3.11  Выводы
  • 4  Открытие новой кофейни
    • 4.1  Выбор локации
    • 4.2  Доля кофеен от общего числа заведений
    • 4.3  Стоимость чашки кофе
    • 4.4  Категория цен
    • 4.5  Выбор часов работы
    • 4.6  Пользовательский рейтинг
    • 4.7  Выводы
  • 5  Презентация

Рынок заведений общественного питания Москвы¶

Исследование рынка заведений общественного питания г. Москва. Поиск интересных особенностей, которые в будущем помогут в выборе подходящего инвесторам места для открытия новых ресторанов.

Датасет с заведениями общественного питания Москвы, составлен на основе данных сервисов Яндекс Карты и Яндекс Бизнес (актуальность информации: лето 2022 года).

In [1]:
! pip install folium
Requirement already satisfied: folium in d:\anaconda\lib\site-packages (0.14.0)
Requirement already satisfied: numpy in d:\anaconda\lib\site-packages (from folium) (1.21.5)
Requirement already satisfied: branca>=0.6.0 in d:\anaconda\lib\site-packages (from folium) (0.6.0)
Requirement already satisfied: jinja2>=2.9 in d:\anaconda\lib\site-packages (from folium) (2.11.3)
Requirement already satisfied: requests in d:\anaconda\lib\site-packages (from folium) (2.27.1)
Requirement already satisfied: MarkupSafe>=0.23 in d:\anaconda\lib\site-packages (from jinja2>=2.9->folium) (2.0.1)
Requirement already satisfied: idna<4,>=2.5 in d:\anaconda\lib\site-packages (from requests->folium) (3.3)
Requirement already satisfied: charset-normalizer~=2.0.0 in d:\anaconda\lib\site-packages (from requests->folium) (2.0.4)
Requirement already satisfied: certifi>=2017.4.17 in d:\anaconda\lib\site-packages (from requests->folium) (2021.10.8)
Requirement already satisfied: urllib3<1.27,>=1.21.1 in d:\anaconda\lib\site-packages (from requests->folium) (1.26.9)
In [2]:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from plotly import graph_objects as go
import plotly.express as px
import folium
from folium import Marker, Map, Choropleth
from folium.plugins import MarkerCluster
from folium.features import CustomIcon
import json

import warnings
warnings.filterwarnings('ignore')
In [3]:
sns.set_palette('pastel')
sns.set_style('whitegrid') 
palette = px.colors.qualitative.Pastel1
In [4]:
moscow_lat, moscow_lng = 55.751244, 37.618423

Обзор и описание данных¶

In [5]:
df = pd.read_csv('Datasets/moscow_places.csv')
In [6]:
with open('C:/Users/Александр/Jupiter Notebook/Yandex/8 - restaurants/Datasets/admin_level_geomap.geojson', 'r') as f:
    geo_json = json.load(f)
    
state_geo = 'C:/Users/Александр/Jupiter Notebook/Yandex/8 - restaurants/Datasets/admin_level_geomap.geojson'
In [7]:
df.head()
Out[7]:
name category address district hours lat lng rating price avg_bill middle_avg_bill middle_coffee_cup chain seats
0 WoWфли кафе Москва, улица Дыбенко, 7/1 Северный административный округ ежедневно, 10:00–22:00 55.878494 37.478860 5.0 NaN NaN NaN NaN 0 NaN
1 Четыре комнаты ресторан Москва, улица Дыбенко, 36, корп. 1 Северный административный округ ежедневно, 10:00–22:00 55.875801 37.484479 4.5 выше среднего Средний счёт:1500–1600 ₽ 1550.0 NaN 0 4.0
2 Хазри кафе Москва, Клязьминская улица, 15 Северный административный округ пн-чт 11:00–02:00; пт,сб 11:00–05:00; вс 11:00... 55.889146 37.525901 4.6 средние Средний счёт:от 1000 ₽ 1000.0 NaN 0 45.0
3 Dormouse Coffee Shop кофейня Москва, улица Маршала Федоренко, 12 Северный административный округ ежедневно, 09:00–22:00 55.881608 37.488860 5.0 NaN Цена чашки капучино:155–185 ₽ NaN 170.0 0 NaN
4 Иль Марко пиццерия Москва, Правобережная улица, 1Б Северный административный округ ежедневно, 10:00–22:00 55.881166 37.449357 5.0 средние Средний счёт:400–600 ₽ 500.0 NaN 1 148.0

Описание наименований столбцов:

name — название заведения;
address — адрес заведения;
category — категория заведения, например «кафе», «пиццерия» или «кофейня»;
hours — информация о днях и часах работы;
lat — широта географической точки, в которой находится заведение;
lng — долгота географической точки, в которой находится заведение;
rating — рейтинг заведения по оценкам пользователей в Яндекс Картах (высшая оценка — 5.0);
price — категория цен в заведении, например «средние», «ниже среднего», «выше среднего» и так далее;
avg_bill — строка, которая хранит среднюю стоимость заказа в виде диапазона, например:

  • «Средний счёт: 1000–1500 ₽»;
  • «Цена чашки капучино: 130–220 ₽»;
  • «Цена бокала пива: 400–600 ₽».
  • и так далее;

middle_avg_bill — число с оценкой среднего чека, которое указано только для значений из столбца avg_bill, начинающихся с подстроки «Средний счёт»
middle_coffee_cup — число с оценкой одной чашки капучино, которое указано только для значений из столбца avg_bill, начинающихся с подстроки «Цена одной чашки капучино»
chain — число, выраженное 0 или 1, которое показывает, является ли заведение сетевым:

  • 0 — заведение не является сетевым
  • 1 — заведение является сетевым

district — административный район, в котором находится заведение, например Центральный административный округ;
seats — количество посадочных мест.

In [8]:
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 8406 entries, 0 to 8405
Data columns (total 14 columns):
 #   Column             Non-Null Count  Dtype  
---  ------             --------------  -----  
 0   name               8406 non-null   object 
 1   category           8406 non-null   object 
 2   address            8406 non-null   object 
 3   district           8406 non-null   object 
 4   hours              7870 non-null   object 
 5   lat                8406 non-null   float64
 6   lng                8406 non-null   float64
 7   rating             8406 non-null   float64
 8   price              3315 non-null   object 
 9   avg_bill           3816 non-null   object 
 10  middle_avg_bill    3149 non-null   float64
 11  middle_coffee_cup  535 non-null    float64
 12  chain              8406 non-null   int64  
 13  seats              4795 non-null   float64
dtypes: float64(6), int64(1), object(7)
memory usage: 919.5+ KB

Некоторые столбцы содержат пропущенные значения. Также данные необходимо проверить на наличие дубликатов и некорректных значений.
В связи с этим проводится предобработка данных.

Предобработка и дополнение данных¶

Обработка дубликатов¶

In [9]:
df = df.drop_duplicates()
In [10]:
df['category'].unique()
Out[10]:
array(['кафе', 'ресторан', 'кофейня', 'пиццерия', 'бар,паб',
       'быстрое питание', 'булочная', 'столовая'], dtype=object)
In [11]:
df['district'].unique()
Out[11]:
array(['Северный административный округ',
       'Северо-Восточный административный округ',
       'Северо-Западный административный округ',
       'Западный административный округ',
       'Центральный административный округ',
       'Восточный административный округ',
       'Юго-Восточный административный округ',
       'Южный административный округ',
       'Юго-Западный административный округ'], dtype=object)
In [12]:
df['price'].unique()
Out[12]:
array([nan, 'выше среднего', 'средние', 'высокие', 'низкие'], dtype=object)
In [13]:
df['chain'].unique()
Out[13]:
array([0, 1], dtype=int64)

Полных и неявных дубликатов не обнаружено.

Далее обрабатываются пропуски:

Обработка пропущенных значений¶

In [14]:
df.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 8406 entries, 0 to 8405
Data columns (total 14 columns):
 #   Column             Non-Null Count  Dtype  
---  ------             --------------  -----  
 0   name               8406 non-null   object 
 1   category           8406 non-null   object 
 2   address            8406 non-null   object 
 3   district           8406 non-null   object 
 4   hours              7870 non-null   object 
 5   lat                8406 non-null   float64
 6   lng                8406 non-null   float64
 7   rating             8406 non-null   float64
 8   price              3315 non-null   object 
 9   avg_bill           3816 non-null   object 
 10  middle_avg_bill    3149 non-null   float64
 11  middle_coffee_cup  535 non-null    float64
 12  chain              8406 non-null   int64  
 13  seats              4795 non-null   float64
dtypes: float64(6), int64(1), object(7)
memory usage: 985.1+ KB

Пропущенные значения столбца price могут быть заполнены на основании даныых middle_avg_bill и middle_coffee_cup

Определяем распределение среднего счета и средней цены чашки кофе:

In [15]:
for cat in df['price'].unique():
    median_bill = df.query('price == @cat')['middle_avg_bill'].quantile([0.25,0.5,0.75])
    print(cat)
    print(median_bill)
    print('--------------------')
nan
0.25   NaN
0.50   NaN
0.75   NaN
Name: middle_avg_bill, dtype: float64
--------------------
выше среднего
0.25    1250.0
0.50    1250.0
0.75    1500.0
Name: middle_avg_bill, dtype: float64
--------------------
средние
0.25    350.0
0.50    500.0
0.75    850.0
Name: middle_avg_bill, dtype: float64
--------------------
высокие
0.25    1750.0
0.50    2000.0
0.75    2500.0
Name: middle_avg_bill, dtype: float64
--------------------
низкие
0.25    150.0
0.50    180.0
0.75    250.0
Name: middle_avg_bill, dtype: float64
--------------------
In [16]:
for cat in df['price'].unique():
    median_coffee = df.query('price == @cat')['middle_coffee_cup'].quantile([0.25,0.5,0.75])
    print(cat)
    print(median_coffee)
    print('--------------------')
nan
0.25   NaN
0.50   NaN
0.75   NaN
Name: middle_coffee_cup, dtype: float64
--------------------
выше среднего
0.25    176.5
0.50    203.0
0.75    229.5
Name: middle_coffee_cup, dtype: float64
--------------------
средние
0.25    160.00
0.50    200.00
0.75    255.75
Name: middle_coffee_cup, dtype: float64
--------------------
высокие
0.25    250.0
0.50    250.0
0.75    250.0
Name: middle_coffee_cup, dtype: float64
--------------------
низкие
0.25    110.0
0.50    139.0
0.75    154.0
Name: middle_coffee_cup, dtype: float64
--------------------

На основании этих данных устанавливаем границы диапазонов для каждой ценовой категории:

In [17]:
avg_prices = pd.DataFrame({'Категория':df['price'].unique(),
                           'min_avg_bill':[None, 1001, 301, 1701, 0],
                           'max_avg_bill':[None, 1700, 1000, 999999, 300],
                           'min_avg_coffee':[None, 201, 156, 241, 0],
                           'max_avg_coffee':[None, 240, 200, 999999, 155]}
                         )
In [18]:
avg_prices
Out[18]:
Категория min_avg_bill max_avg_bill min_avg_coffee max_avg_coffee
0 NaN NaN NaN NaN NaN
1 выше среднего 1001.0 1700.0 201.0 240.0
2 средние 301.0 1000.0 156.0 200.0
3 высокие 1701.0 999999.0 241.0 999999.0
4 низкие 0.0 300.0 0.0 155.0

Далее заполняем датафрейм в соответствии с определенными выше границами:

In [19]:
low = list(df[np.logical_and(df['price'].isnull() == True, 
                             df['middle_avg_bill'] >= avg_prices['min_avg_bill'][4],
                             df['middle_avg_bill'] <= avg_prices['max_avg_bill'][4])]['price'].index)
med = list(df[np.logical_and(df['price'].isnull() == True, 
                             df['middle_avg_bill'] >= avg_prices['min_avg_bill'][2],
                             df['middle_avg_bill'] <= avg_prices['max_avg_bill'][2])]['price'].index)
med_high = list(df[np.logical_and(df['price'].isnull() == True, 
                             df['middle_avg_bill'] >= avg_prices['min_avg_bill'][1],
                             df['middle_avg_bill'] <= avg_prices['max_avg_bill'][1])]['price'].index)
high = list(df[np.logical_and(df['price'].isnull() == True, 
                             df['middle_avg_bill'] >= avg_prices['min_avg_bill'][3],
                             df['middle_avg_bill'] <= avg_prices['max_avg_bill'][3])]['price'].index)
In [20]:
for index in low:
    df['price'][index] = 'низкие'

for index in med:
    df['price'][index] = 'средние'

for index in med_high:
    df['price'][index] = 'выше среднего'
    
for index in high:
    df['price'][index] = 'высокие'
In [21]:
low_coffee = list(df[np.logical_and(df['price'].isnull() == True, 
                             df['middle_coffee_cup'] >= avg_prices['min_avg_coffee'][4],
                             df['middle_coffee_cup'] <= avg_prices['max_avg_coffee'][4])]['price'].index)
med_coffee = list(df[np.logical_and(df['price'].isnull() == True, 
                             df['middle_coffee_cup'] >= avg_prices['min_avg_coffee'][2],
                             df['middle_coffee_cup'] <= avg_prices['max_avg_coffee'][2])]['price'].index)
med_high_coffee = list(df[np.logical_and(df['price'].isnull() == True, 
                             df['middle_coffee_cup'] >= avg_prices['min_avg_coffee'][1],
                             df['middle_coffee_cup'] <= avg_prices['max_avg_coffee'][1])]['price'].index)
high_coffee = list(df[np.logical_and(df['price'].isnull() == True, 
                             df['middle_coffee_cup'] >= avg_prices['min_avg_coffee'][3],
                             df['middle_coffee_cup'] <= avg_prices['max_avg_coffee'][3])]['price'].index)
In [22]:
for index in low_coffee:
    df['price'][index] = 'низкие'

for index in med_coffee:
    df['price'][index] = 'средние'

for index in med_high_coffee:
    df['price'][index] = 'выше среднего'
    
for index in high_coffee:
    df['price'][index] = 'высокие'
In [23]:
df.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 8406 entries, 0 to 8405
Data columns (total 14 columns):
 #   Column             Non-Null Count  Dtype  
---  ------             --------------  -----  
 0   name               8406 non-null   object 
 1   category           8406 non-null   object 
 2   address            8406 non-null   object 
 3   district           8406 non-null   object 
 4   hours              7870 non-null   object 
 5   lat                8406 non-null   float64
 6   lng                8406 non-null   float64
 7   rating             8406 non-null   float64
 8   price              4042 non-null   object 
 9   avg_bill           3816 non-null   object 
 10  middle_avg_bill    3149 non-null   float64
 11  middle_coffee_cup  535 non-null    float64
 12  chain              8406 non-null   int64  
 13  seats              4795 non-null   float64
dtypes: float64(6), int64(1), object(7)
memory usage: 1.2+ MB

В столбце price удалось заполнить около 700 значений.

Остальные значения заполнить не удается, поэтому оставляем пустыми.

Дополнение данных¶

Из столбца с адресом извлекается название улицы для последующего анализа и записывается в отдельный столбец.

In [24]:
df['street'] = df['address']
In [25]:
for index in range(len(df['street'])):
    df['street'][index] = df['street'][index].split(',')[1].strip()

Также из информации о часах работы извлекаются данные о заведениях, работающих ежедневно и круглосуточно.

In [26]:
df['is_24/7'] = 0
In [27]:
for index in range(len(df['hours'])):
    if df['hours'][index] == 'ежедневно, круглосуточно':
        df['is_24/7'][index] = 1
In [28]:
df['is_24/7'].sum()
Out[28]:
730
In [29]:
df.head()
Out[29]:
name category address district hours lat lng rating price avg_bill middle_avg_bill middle_coffee_cup chain seats street is_24/7
0 WoWфли кафе Москва, улица Дыбенко, 7/1 Северный административный округ ежедневно, 10:00–22:00 55.878494 37.478860 5.0 NaN NaN NaN NaN 0 NaN улица Дыбенко 0
1 Четыре комнаты ресторан Москва, улица Дыбенко, 36, корп. 1 Северный административный округ ежедневно, 10:00–22:00 55.875801 37.484479 4.5 выше среднего Средний счёт:1500–1600 ₽ 1550.0 NaN 0 4.0 улица Дыбенко 0
2 Хазри кафе Москва, Клязьминская улица, 15 Северный административный округ пн-чт 11:00–02:00; пт,сб 11:00–05:00; вс 11:00... 55.889146 37.525901 4.6 средние Средний счёт:от 1000 ₽ 1000.0 NaN 0 45.0 Клязьминская улица 0
3 Dormouse Coffee Shop кофейня Москва, улица Маршала Федоренко, 12 Северный административный округ ежедневно, 09:00–22:00 55.881608 37.488860 5.0 средние Цена чашки капучино:155–185 ₽ NaN 170.0 0 NaN улица Маршала Федоренко 0
4 Иль Марко пиццерия Москва, Правобережная улица, 1Б Северный административный округ ежедневно, 10:00–22:00 55.881166 37.449357 5.0 средние Средний счёт:400–600 ₽ 500.0 NaN 1 148.0 Правобережная улица 0

Данные обработаны и дополнены, можно приступать к анализу.

Анализ данных¶

Распределение по категориям¶

In [30]:
category_pivot = df.pivot_table(index='category', 
                                values='name', 
                                aggfunc='count').sort_values(by='name', ascending=False)
In [31]:
category_pivot
Out[31]:
name
category
кафе 2378
ресторан 2043
кофейня 1413
бар,паб 765
пиццерия 633
быстрое питание 603
столовая 315
булочная 256
In [32]:
pie_category = go.Figure(data=[go.Pie(labels=category_pivot.index, 
                                      values=category_pivot['name'],
                                      marker_colors=palette,
                                      title = 'Распределение заведений по категориям'
                                     )
                              ]
                        )

pie_category.show() 

Из табличных и графических данных видно, что наибольшую долю составляют кафе и рестораны, также значительную часть заведений занимают кофейни.

Количество посадочных мест¶

In [33]:
plt.figure(figsize=(12, 5))

sns.barplot(x='category', 
            y='seats', 
            data=df
           )

plt.title('Зависимость количества мест от типа заведения')
plt.xlabel('Категории')
plt.ylabel('Количество мест');

На графике указано среднее количество мест в заведении каждой категории.
Заметно, что бары наиболее вместительные, а пекарни и столовые имеют наибольший диапазон значений.

Сетевые и не сетевые заведения¶

Ниже представлено две диаграммы:

  1. Соотношение по количеству заведений
  2. Соотношение по количеству брендов
In [34]:
pie_category = go.Figure(data=[go.Pie(labels=['сетевые', 'не сетевые'], 
                                      values=[df.query('chain == 1')['name'].count(),
                                              df.query('chain == 0')['name'].count()],
                                      marker_colors=palette,
                                      title = 'Соотношение количества сетевых и не сетевых заведений (по количеству точек)'
                                     )
                              ]
                        )
pie_category.show() 
In [35]:
pie_category = go.Figure(data=[go.Pie(labels=['сетевые', 'не сетевые'], 
                                      values=[df.query('chain == 1')['name'].nunique(),
                                              df.query('chain == 0')['name'].nunique()],
                                      marker_colors=palette,
                                      title = 'Соотношение количества сетевых и не сетевых заведений (по количеству наименований)'
                                     )
                              ]
                        )
pie_category.show() 

Как видно, сетей всего 13%, тем не менее, они занимают почти 40% рынка.

Распределение сетевых заведений по категориям¶

Для оценки данного распределения подсчитываются все заведения каждой категории, а также количество сетевых.
Далее находится соотношение (условно - вероятность того, что заведение определеннй категории является сетевым.)

In [36]:
category_pivot_chains = df.query('chain == 1').pivot_table(index='category', 
                                values='name', 
                                aggfunc='count').sort_values(by='name', ascending=False)
In [37]:
category_pivot_chains = category_pivot.merge(category_pivot_chains, on='category')
In [38]:
category_pivot_chains.columns=('all_places', 'chain_places')
category_pivot_chains['chain_percent'] = round(category_pivot_chains['chain_places'] / \
                                               category_pivot_chains['all_places'], 2)
In [39]:
category_pivot_chains
Out[39]:
all_places chain_places chain_percent
category
кафе 2378 779 0.33
ресторан 2043 730 0.36
кофейня 1413 720 0.51
бар,паб 765 169 0.22
пиццерия 633 330 0.52
быстрое питание 603 232 0.38
столовая 315 88 0.28
булочная 256 157 0.61
In [40]:
plt.figure(figsize=(12, 5))

sns.barplot(x=category_pivot_chains['chain_percent'], 
            y=category_pivot_chains.index
           )

plt.title('Зависимость количества сетевых заведений от типа')
plt.xlabel('Категории')
plt.ylabel('Количество сетевых заведений');

Чаще всего булочные, пиццерии и кофейни являются сетевыми (небольшие заведения), в то время как бары и столовые чаще имеют только одну точку.

Крупнейшие сети¶

Отбираются самые крупные сети по количеству заведений.

In [41]:
top_15_chains = df.pivot_table(index=['name', 'category'],
                               values='chain',
                               aggfunc='count'
                              )
top_15_chains.columns = ['points_count']

top_15_chains = top_15_chains.sort_values(by='points_count', 
                                            ascending=False
                                           )
In [42]:
top_15_chains.head(20)
Out[42]:
points_count
name category
Кафе кафе 159
Шоколадница кофейня 119
Домино'с Пицца пиццерия 76
Додо Пицца пиццерия 74
One Price Coffee кофейня 71
Яндекс Лавка ресторан 69
Cofix кофейня 65
Prime ресторан 49
КОФЕПОРТ кофейня 42
Кулинарная лавка братьев Караваевых кафе 39
Теремок ресторан 36
Ресторан ресторан 33
Шаурма быстрое питание 32
CofeFest кофейня 31
Чайхана кафе 26
Буханка булочная 25
Drive Café кафе 24
Кофемания кофейня 22
Столовая столовая 22
Cinnabon кофейня 20

В таблицу попали не только бренды, но и общие названия, такие как Кафе, Ресторан и.т.д
Эти строки необходимо исключить.

In [43]:
top_15_chains = top_15_chains.drop(index=['Кафе', 'Хинкальная', 'Шаурма', 'Ресторан', 'Столовая'], axis=0)
top_15_chains = top_15_chains.head(15)
In [44]:
top_15_chains
Out[44]:
points_count
name category
Шоколадница кофейня 119
Домино'с Пицца пиццерия 76
Додо Пицца пиццерия 74
One Price Coffee кофейня 71
Яндекс Лавка ресторан 69
Cofix кофейня 65
Prime ресторан 49
КОФЕПОРТ кофейня 42
Кулинарная лавка братьев Караваевых кафе 39
Теремок ресторан 36
CofeFest кофейня 31
Чайхана кафе 26
Буханка булочная 25
Drive Café кафе 24
Кофемания кофейня 22
In [45]:
plt.figure(figsize=(12, 5))

sns.barplot(y=top_15_chains['points_count'],
            x=top_15_chains.index.get_level_values(0)
             )

plt.title('Топ-15 сетей по количеству заведений')
plt.xlabel('Название сети')
plt.ylabel('Количество заведение')
plt.xticks(rotation=90);

Шоколадница - самая многочисленная сеть.
Также можно заметить, что в топе в основном кофейни.

Посмотрим количество сетей каждой категории:

In [46]:
plt.figure(figsize=(12, 5))

sns.countplot(x=top_15_chains.index.get_level_values(1),
              data=top_15_chains
             )

plt.title('Количество сетей каждого типа среди топ-15')
plt.xlabel('Категории')
plt.ylabel('Количество сетей');

Данные подтверждают предыдущий вывод - в топ-15 сетей ни одного бара и столовой, в то время как кофеен - целых 6.

Районы, кварталы¶

In [47]:
plt.figure(figsize=(12, 5))

sns.countplot(x='district',
              data=df
             )

plt.title('Распределение заведений по административным округам')
plt.xlabel('Административный округ')
plt.ylabel('Количество заведений')
plt.xticks(rotation=90);
In [48]:
district_df = df.groupby('district', as_index=False)['name'].agg('count')
district_df
Out[48]:
district name
0 Восточный административный округ 798
1 Западный административный округ 851
2 Северный административный округ 900
3 Северо-Восточный административный округ 891
4 Северо-Западный административный округ 409
5 Центральный административный округ 2242
6 Юго-Восточный административный округ 714
7 Юго-Западный административный округ 709
8 Южный административный округ 892
In [49]:
m_districts_total = Map(location=[moscow_lat, moscow_lng], zoom_start=10)
In [50]:
Choropleth(
    geo_data=state_geo,
    data=district_df,
    columns=['district', 'name'],
    key_on='feature.name',
    fill_color='YlGn',
    fill_opacity=0.8,
    legend_name='Количество заведений по районам',
).add_to(m_districts_total);
In [51]:
m_districts_total
Out[51]:
Make this Notebook Trusted to load map: File -> Trust Notebook

Неудивительно, что в центе заведений общественного питания почти втрое больше.
Остальные районы, кроме Северо-Западного содержат примерно одинаковое количество точек.

Распределение по категориям внутри каждого района

In [52]:
plt.figure(figsize=(6, 12))

sns.countplot(y='district',
              data=df,
              hue='category'
             )

plt.title('Распределение заведений каждой категории по административным округам')
plt.ylabel('Административный округ, Категория')
plt.xlabel('Количество заведений');

Соотношение категорий похоже для всех районов.
Однако, в центральном заметно превалирование количества ресторанов, что объясняется престижностью локации и, следовательно, классом заведений.

Пользовательский рейтинг¶

Изучается зависимость рейтинга от различных факторов:

  • Тип заведения
  • Район
  • Принадлежность к сети
In [53]:
plt.figure(figsize=(12, 5))
plt.axis([0,0,3.5,4.5])

sns.barplot(x = 'category',
            y='rating',
            data = df
           )

plt.title('Средние рейтинги для каждой категории заведений')
plt.xlabel('Категория')
plt.ylabel('Средний рейтинг')
plt.xticks(rotation=90);

Закономерно, точки быстрого питания имеют более низкие рейтинги, т.к. их фокус заключается в скорости обслуживания.
Бары и рестораны наоборот, стараются сделать посещение максимально комфортным, поэтому в среднем имеют более высокие рейтинги.

Cетевые и не сетевые заведения¶

In [54]:
plt.figure(figsize=(12, 5))
plt.axis([0,0,3.5,4.5])

sns.barplot(x = 'category',
            y='rating',
            data = df,
            hue='chain'
           )

plt.title('Средние рейтинги для каждой категории заведений (сравнение сетевых и не сетевых)')
plt.xlabel('Категория')
plt.ylabel('Средний рейтинг')
plt.xticks(rotation=90);

Интересное наблюдение - среди кафе пользователи предпочитают сетевые заведения, в то время как при посещении ресторанов или кофеен отдают предпочтение локальным точкам.

Зависимость рейтинга от района¶

In [55]:
rating_df = df.groupby('district', as_index=False)['rating'].agg('median')
rating_df
Out[55]:
district rating
0 Восточный административный округ 4.3
1 Западный административный округ 4.3
2 Северный административный округ 4.3
3 Северо-Восточный административный округ 4.2
4 Северо-Западный административный округ 4.3
5 Центральный административный округ 4.4
6 Юго-Восточный административный округ 4.2
7 Юго-Западный административный округ 4.3
8 Южный административный округ 4.3
In [56]:
m_chor_rating = Map(location=[moscow_lat, moscow_lng], zoom_start=10)
In [57]:
Choropleth(
    geo_data=state_geo,
    data=rating_df,
    columns=['district', 'rating'],
    key_on='feature.name',
    fill_color='YlGn',
    fill_opacity=0.8,
    legend_name='Медианный рейтинг заведений по районам',
).add_to(m_chor_rating);
In [58]:
m_chor_rating
Out[58]:
Make this Notebook Trusted to load map: File -> Trust Notebook

Средний рейтинг заведений в центре выше, однако разброс данных незначительный (всего 0,2 балла)

Места на карте¶

Все указанные в датасете заведения отмечены на карте ниже.
Для удобства навигации, каждая категория заведений имеет свою иконку.

In [59]:
m_clust = Map(location=[moscow_lat, moscow_lng], zoom_start=10)
In [60]:
icons = {'кафе':'https://img.icons8.com/color/256/restaurant-building.png',
         'ресторан':'https://img.icons8.com/external-flaticons-flat-flat-icons/256/external-restaurant-vegan-and-vegetarian-flaticons-flat-flat-icons.png',
         'кофейня':'https://img.icons8.com/office/256/coffee.png',
         'пиццерия':'https://img.icons8.com/color/256/pizza.png',
         'бар,паб':'https://img.icons8.com/external-wanicon-lineal-color-wanicon/256/external-pub-st-patrick-day-wanicon-lineal-color-wanicon.png',
         'быстрое питание':'https://img.icons8.com/external-kosonicon-lineal-color-kosonicon/256/external-fast-food-back-to-school-kosonicon-lineal-color-kosonicon.png',
         'булочная':'https://img.icons8.com/color/256/bakery.png',
         'столовая':'https://img.icons8.com/color-glass/256/dining-room.png'
        }
In [61]:
marker_cluster = MarkerCluster().add_to(m_clust)
In [62]:
def create_clusters(row):
    for cat in df['category'].unique():
        if row['category'] == cat:
            icon_url = icons.get(cat)
            icon = CustomIcon(icon_url, icon_size=(30, 30))
        
            Marker([row['lat'], 
                    row['lng']],
                    popup=f"{row['name']} {row['rating']}",
                    icon=icon,
                  ).add_to(marker_cluster)
In [63]:
df.apply(create_clusters, axis=1);

m_clust
Out[63]:
Make this Notebook Trusted to load map: File -> Trust Notebook

Популярные улицы¶

Исследуется, на каких улицах находися наибольшее количество заведений общественного питания

In [64]:
top_15_streets = df.pivot_table(index='street', 
                                values='name', 
                                aggfunc='count'
                               ).sort_values(by='name', 
                                             ascending=False
                                            ).head(15)
In [65]:
plt.figure(figsize=(12, 5))

sns.barplot(x=top_15_streets['name'], 
            y=top_15_streets.index
           )

plt.title('Топ-15 улиц по количеству заведений общественного питания')
plt.xlabel('Количество точек')
plt.ylabel('Улица');

Разумеется, в Топ-15 вошли крупные магистрали с большим потоком людей.

In [66]:
top_15_streets_data = df.query('street in @top_15_streets.index')

Распределение заведений по категориям

In [67]:
plt.figure(figsize=(6, 15))

sns.countplot(y='street',
              data=top_15_streets_data,
              hue='category'
             )

plt.title('Распределений заведений по категориям на топ-15 улиц')
plt.ylabel('Улица')
plt.xlabel('Количество точек');

На некоторых улицах преобладают заведения определенной категории (например, кафе на МКАДе), в то время как на других распределение более равномерное.

Непопулярные улицы¶

Далее отбираются и изучаются улицы, на которых находится только одно заведение общественного питания

In [68]:
only_1_streets = df.pivot_table(index='street', 
                                values='name', 
                                aggfunc='count'
                               ).query('name == 1')
In [69]:
only_1_streets_data = df.query('street in @only_1_streets.index')
In [70]:
m_only_1 = Map(location=[moscow_lat, moscow_lng], zoom_start=10)
In [71]:
marker_cluster_2 = MarkerCluster().add_to(m_only_1)
In [72]:
def create_clusters_2(row):
    for cat in df['category'].unique():
        if row['category'] == cat:
            icon_url = icons.get(cat)
            icon = CustomIcon(icon_url, icon_size=(30, 30))
        
            Marker([row['lat'], 
                    row['lng']],
                    popup=f"{row['name']} {row['rating']}",
                    icon=icon,
                  ).add_to(marker_cluster_2)

Данные объекты наносяся на карту:

In [73]:
only_1_streets_data.apply(create_clusters_2, axis=1)

m_only_1
Out[73]:
Make this Notebook Trusted to load map: File -> Trust Notebook

Как видно, распределение этих объектов идентично распределению всех заведений.
Улицы, на которых они располагаются непольшие, поэтому на них и было открыло не более 1го заведения.

In [74]:
only_1_category_pivot = only_1_streets_data.pivot_table(index='category', 
                                                        values='name', 
                                                        aggfunc='count'
                                                       ).sort_values(by='name', 
                                                                     ascending=False
                                                                    )
In [75]:
pie_category = go.Figure(data=[go.Pie(labels=only_1_category_pivot.index, 
                                      values=only_1_category_pivot['name'],
                                      marker_colors=palette,
                                      title = 'Распределение заведений по категориям (на улицах с 1-м заведением)'
                                     )
                              ]
                        )

pie_category.show() 

Распределение по категориям среди данных улиц похоже на общее распределение, однако доля кафе на них выше.

Средний чек¶

Исследуется зависимость среднего чека от различных параметров:

  1. Район
  2. Тип заведения
  3. Принадлежность к сети

Зависимость среднего чека от локации¶

In [76]:
avg_bill_df = df.groupby('district', as_index=False)['middle_avg_bill'].agg('median')
avg_bill_df
Out[76]:
district middle_avg_bill
0 Восточный административный округ 575.0
1 Западный административный округ 1000.0
2 Северный административный округ 650.0
3 Северо-Восточный административный округ 500.0
4 Северо-Западный административный округ 700.0
5 Центральный административный округ 1000.0
6 Юго-Восточный административный округ 450.0
7 Юго-Западный административный округ 600.0
8 Южный административный округ 500.0

Из таблице видно, что средний чек заведений в центре и на западе выше, однако следует визуализировать данную информацию:

In [77]:
m_chor_bill = Map(location=[moscow_lat, moscow_lng], zoom_start=10)
In [78]:
Choropleth(
    geo_data=state_geo,
    data=avg_bill_df,
    columns=['district', 'middle_avg_bill'],
    key_on='feature.name',
    fill_color='YlGn',
    fill_opacity=0.8,
    legend_name='Медианный средний чек заведений по районам',
).add_to(m_chor_bill);
In [79]:
m_chor_bill
Out[79]:
Make this Notebook Trusted to load map: File -> Trust Notebook

Отличие среднего чека в центре закономерно.
Высоки средний чек в Западном АО может объясняться вхождением в его состав Рублевского шоссе и аэропорта "Внуково"

Зависимость среднего чека от типа заведения¶

In [80]:
plt.figure(figsize=(7, 12))

sns.barplot(y='category',
            x='middle_avg_bill',
              data=df
             )

plt.title('Средний чек для каждой категории')
plt.ylabel('Тип заведения')
plt.xlabel('Средний чек');

Высокий средний чек ресторанов и баров объясняется как высоким классом обслуживания, так и тем, что их посещают в компании.
Столовые и точки продажи фаст-фуда в основном ориентированы на индивидуальных посетителей и предлагают более низкие цены, соответственно ниже и средний чек.

In [81]:
plt.figure(figsize=(15, 7))

sns.barplot(x='category',
            y='middle_avg_bill',
            data=df,
            hue='chain'
             )

plt.title('Сравнение среднего чека сетевых и не сетевых заведений')
plt.xlabel('Тип заведения')
plt.ylabel('Средний чек');

Несетевые рестораны и бары в среднем дороже, вероятно это объясняется их уникальностью.
Среди кофеен и булочных наоборот, сети имеют возможность усатанвливать более высокие цены благодаря узнаваемости бренда.

Выводы¶

В наборе данных представлено множество заведений общественного уровня, разного типа, уровня цен, локаций и.т.д

Заведения распределены по локациям достаточно равномерно, их плотность увеличивается ближе к центру города. Наибольшее количество заведений имеют категории "Кафе" и "Ресторан", ввиду их универсальности и широкого меню:

  • Около 25% всех заведений расположена в центре (в основном там находятся рестораны, кафе, кофейни и бары) В центре количество более дорогих и эксклюзивных объектов выше, средняя стоимость блюд также повышается.

Также тип заведений в значительной мере зависит от того, где располагается объект.

Заведения в разных окрестных районах имеют схожие рейтинги

Также стоит отметить, что ценовая политика объектов различных категорий зависит от того, являются ли они сетевыми или индивидуальными.

Изложенные выше результаты в дальнейшем используются для составления бизнес-плана открытия новой кофейни.

Открытие новой кофейни¶

Перед открытием нового заведения необходимо изучить и определить:

  • наиболее подходящую локацию
  • ценовой диапазон предлагаемых напитков
  • предполагаемые часы работы

и другие ключевые параметры.

Выбор локации¶

In [82]:
coffee = df.query('category == "кофейня"')

coffee_count = coffee.groupby('district', as_index=False)['name'].agg('count')
coffee_count
Out[82]:
district name
0 Восточный административный округ 105
1 Западный административный округ 150
2 Северный административный округ 193
3 Северо-Восточный административный округ 159
4 Северо-Западный административный округ 62
5 Центральный административный округ 428
6 Юго-Восточный административный округ 89
7 Юго-Западный административный округ 96
8 Южный административный округ 131
In [83]:
coffee_count['name'].sum()
Out[83]:
1413

Сводная таблица с количеством кофеен в каждом районе.

Ниже приведена ее визуализация

In [84]:
m_chor_coffee = Map(location=[moscow_lat, moscow_lng], zoom_start=10)
In [85]:
Choropleth(
    geo_data=state_geo,
    data=coffee_count,
    columns=['district', 'name'],
    key_on='feature.name',
    fill_color='YlGn',
    fill_opacity=0.8,
    legend_name='Количество кофеен по районам'
).add_to(m_chor_coffee);
In [86]:
m_chor_coffee
Out[86]:
Make this Notebook Trusted to load map: File -> Trust Notebook

Количество кофеен в центре заметно выше, чем в других районах.

Для выбора наиболее подходящего места следует также изучить данные о проходимости.

Доля кофеен от общего числа заведений¶

In [87]:
district_df_coffee = district_df.merge(coffee_count, on='district')
In [88]:
district_df_coffee.columns=('district', 'total_places', 'coffee_places')
In [89]:
district_df_coffee['coffee_percent'] = round(district_df_coffee['coffee_places'] / \
                                            district_df_coffee['total_places'], 2)
In [90]:
district_df_coffee = district_df_coffee.sort_values(by='coffee_percent', ascending=False)
In [91]:
plt.figure(figsize=(12, 5))

sns.barplot(x=district_df_coffee['coffee_percent'], 
            y=district_df_coffee['district']
           )

plt.title('Доля кофеен от числа объектов')
plt.xlabel('Соотношение')
plt.ylabel('Район');

Стоимость чашки кофе¶

Стоит оценить среднюю стоимость чашки кофе имеющихся кофеен для определения целевой цены:

Поскольку данные могут содержать "выбросы", данные стоит предварительно отфильтровать

In [92]:
def quantile_filter (data, col, min, max):
        
    top_filter = data[col].quantile(max)
    data = data[data[col] <= top_filter]
    
    bottom_filter = data[col].quantile(min)
    data = data[data[col] >= bottom_filter]
    
    return data
In [93]:
coffee = quantile_filter (coffee, 'middle_coffee_cup', 0.01, 0.99)
In [94]:
plt.figure(figsize=(12, 6))

sns.distplot(coffee['middle_coffee_cup'], bins=20)

plt.title('Распределений кофеен по стоимости чашки кофе')
plt.xlabel('Стоимость 1 чашки кофе, р.')
plt.ylabel('Количество кофеен');

Из графика можно заметить 3-4 пика.
Вероятно, они соответствуют ценовым категориям данных кофеен.
Следует изучить ценовые категории, а также их расположение, что поможет в выборе локации.

Категория цен¶

In [95]:
price_category_pivot = coffee.pivot_table(index='price', 
                                values='name', 
                                aggfunc='count').sort_values(by='name', ascending=False)
In [96]:
price_category_pivot
Out[96]:
name
price
средние 256
низкие 196
высокие 39
выше среднего 24
In [97]:
pie_price_category = go.Figure(data=[go.Pie(labels=price_category_pivot.index, 
                                      values=price_category_pivot['name'],
                                      marker_colors=palette,
                                      title = 'Распределение кофеен по ценовым категориям'
                                     )
                              ]
                        )

pie_price_category.show() 

Кофеен с низкими и умеренными ценами заметно больше.
Далее следует изучить распределение кофеен по районам и ценовым категориям.

In [98]:
plt.figure(figsize=(7, 12))

sns.countplot(y='district',
              data=coffee,
              hue='price'
             )

plt.title('Распределение кофеен по районам и ценовым категориям')
plt.ylabel('Район, категория')
plt.xlabel('Количество точек');

Исходя из полученной информации можно сделать вывод, что необходимо связывать выбираемую локацию и планируемум ценовую категорию кофейни.
В некоторый районах преобладают кофейни с низкими ценами, в других - со средними.
В определенных районах кофейни определнных ценовых категорий не представлены вовсе
(стоит также уточнить, это показатель пустующей ниши или отсутствия спроса).

Выбор часов работы¶

Исследуется количество кофеен, работающих ежедневно и круглосуточно.
Также изучается их распределение по районам.

In [99]:
coffee_24_7 = coffee[coffee['is_24/7'] == 1]
In [100]:
coffee_24_7_count = coffee_24_7.groupby('district', as_index=False)['name'].agg('count')
coffee_24_7_count
Out[100]:
district name
0 Восточный административный округ 1
1 Западный административный округ 3
2 Северный административный округ 1
3 Северо-Восточный административный округ 1
4 Центральный административный округ 5
5 Юго-Западный административный округ 4
In [101]:
coffee_24_7_count['name'].sum()
Out[101]:
15

Как видно из таблицы, кофеен с графиком 24/7 довольно мало.

Для выбора наиболее подходящих часов работы следует дополнительно изучить, связано ли это с отсутствием спроса
(вероятно, клиенты не заинтересованы в чашке кофе поздно вечером или ночью)

Пользовательский рейтинг¶

Исследуются значения рейтинга кофеен, расположенных в различных районах Москвы

In [105]:
coffee_rating = coffee.groupby('district', as_index=False)['rating'].agg('median')
coffee_rating
Out[105]:
district rating
0 Восточный административный округ 4.3
1 Западный административный округ 4.2
2 Северный административный округ 4.3
3 Северо-Восточный административный округ 4.3
4 Северо-Западный административный округ 4.4
5 Центральный административный округ 4.3
6 Юго-Восточный административный округ 4.3
7 Юго-Западный административный округ 4.3
8 Южный административный округ 4.3

Данные практически идентичны, средний рейтинг кофеен - 4,3 балла.

Выводы¶

  1. Всего в Москве расположено порядка 1400 кофеен (по состоянию на лето 2022г.)
  2. Треть из них расположена в центре.
  3. Примерно 1 из 25 кофеен работает в режиме 24/7
    • Это может быть связано в том числе с отсутствием спроса
  4. Медианный рейтинг кофеен 4,3 балла и не зависит от локации
  5. Подавляющее большинство кофеен имеют категорию цен "низкие" или "средние"
    • Это также может быть связано с отсутствием спроса на более дорогие кофейни
  6. Средняя стоимость чашки кофе - около 150р.

Презентация¶

Файл с презентацией